package airacrobats.sound;

import airacrobats.io.data.SoundData;
import java.util.List;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;

/**
 * This class represents a sound.
 */
public class Sound
{
    /**
     * The max gain of the sound.
     */
    public final static float MAX_GAIN = -1.0f;
    /**
     * The min gain of the sound.
     */
    public final static float MIN_GAIN = -40.0f;
    /**
     * The mixer index used to play the sounds.
     */
    private final static int MIXER_INDEX = 4;

    /**
     * The sound data.
     */
    private SoundData soundData;
    /**
     * Pan control.
     */
    private FloatControl panControl;
    /**
     * Gain control.
     */
    private FloatControl gainControl;
    /**
     * Mixer.
     */
    private static Mixer mixer;
    /**
     * Flag that indicates if the sound is not playing.
     */
    private boolean stopped;
    /**
     * Gain.
     */
    private float gain;
    /**
     * Pan.
     */
    private float pan;
    /**
     * Single play flag.
     */
    private boolean singlePlay;

    /**
     * @param soundData The sound data.
     * @param singlePlay Single play flag.
     * @param initialGain Initial gain, that will not be affected to the gain range.
     */
    public Sound(SoundData soundData, boolean singlePlay, float initialGain)
    {
        this.soundData = soundData;
        this.singlePlay = singlePlay;
        this.gain = initialGain;
        if (mixer == null)
        {
            Mixer.Info[] mixerInfo = AudioSystem.getMixerInfo();
            mixer = AudioSystem.getMixer(mixerInfo[MIXER_INDEX]);
        }
        stopped = true;
    }

    /**
     * @param soundData The sound data.
     */
    public Sound(SoundData soundData)
    {
        this(soundData, false, 0.0f);
    }

    /**
     * Plays the sound.
     * @throws LineUnavailableException Thrown if an error occurs.
     */
    public void play() throws LineUnavailableException
    {
        stop();
        
        final SourceDataLine line = getLine(soundData.getDecodedFormat());
        gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
        panControl = (FloatControl) line.getControl(FloatControl.Type.PAN);

        if (line != null)
        {
            Thread t = new Thread()
            {
                @Override
                public void run()
                {
                    List<byte[]> bytes = soundData.getBytes();
                    line.start();
                    
                    while (!stopped)
                    {   
                        for (byte[] b : bytes)
                        {
                            if (stopped)
                                break;

                            gainControl.setValue(gain);
                            panControl.setValue(pan);

                            line.write(b, 0, b.length);
                        }

                        if (singlePlay)
                            break;
                    }
                    if (!singlePlay || stopped)
                        line.flush();
                    else
                        line.drain();
                    line.stop();
                    line.close();
                }
            };

            stopped = false;
            t.start();
        }
    }

    /**
     * @return True if the sound is playing.
     */
    public boolean isPlaying()
    {
        return !stopped;
    }

    /**
     * Stop the sound.
     */
    public void stop()
    {
        stopped = true;
    }

    /**
     * @param gain The new gain (It will be set on the min/max range).
     */
    public void setGain(float gain)
    {
        this.gain = Math.min(gain, MAX_GAIN);
        this.gain = Math.max(this.gain, MIN_GAIN);
    }

    /**
     * @return The gain.
     */
    public float getGain()
    {
        return gain;
    }

    /**
     * @param pan The new pan.
     */
    public void setPan(float pan)
    {
        this.pan = pan;
    }

    /**
     * @return The pan.
     */
    public float getPan()
    {
        return pan;
    }

    /**
     * Creates the line of the audio.
     * @param audioFormat The audio format.
     * @return The line.
     * @throws LineUnavailableException Thrown if an error occurs.
     */
    private SourceDataLine getLine(AudioFormat audioFormat) throws LineUnavailableException
    {
        SourceDataLine res = null;
        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
        res = (SourceDataLine) mixer.getLine(info);
        res.open(audioFormat);
        return res;
    }
}
